iT邦幫忙

2023 iThome 鐵人賽

DAY 23
1
Mobile Development

Flutter 從零到實戰 - 30 天の學習筆記系列 第 23

[Day 23] 實戰新聞 APP - 製作本機通知訊息吧

  • 分享至 

  • xImage
  •  

當我們在使用手機時經常會收到來自於不同應用程式的各種通知。包括由遠程的服務發送的通知(如:Line 訊息、Facebook 好友邀請通知等),以及來自於本機的通知(如:系統使用空間不足、系統更新完成等)。

這兩者在本質上有著很大的區別:

  1. 遠程通知是透過遠程伺服器觸發通知事件並發送給您的裝置,應用程式只需要註冊接收通知
  2. 本地通知是由應用程式內部程式碼發送的通知,像是設定鬧鐘等等

我們應用程式目前有使用到 firebase 的authentication 服務,其實 firebase 也有提供推送遠程通知的功能 Firebase Cloud Messaging 。要使其在iOS 與 Android 裝置上實現是相當容易的,Firebase 官方也有提供完整的文件教各位如何實現。但難就難在若要於 iOS 裝置上接收遠程通知,同樣的得先加入 Apple Developer Program,花一波錢成為開發者,才能繼續將設置完成... 所以我們目前也同樣先繞過這個方案,先實作來自於本機的通知訊息吧!

首先請先下載以下套件

flutter pub add flutter_local_notifications

該套件是一個可以輕易實現本機通知的套件,針對跨平台的支援也相當不錯。因此我們將會以此作為我們通知的媒介。

不過想要在 Android 及 iOS 平台上使用通知服務,還需要分別對兩者進行一些設定。

Android 設定

請開啟 android/app/src/main/AndroidManifest.xml 檔案,我們需要在 <activity> 標籤中加入以下設定:

<activity android.name=".MainActivity" ....>
  <!-- 會有 meta-data 及其他 intent-filter,就直接加在後方 -->
  <intent-filter>
      <action android:name="FLUTTER_NOTIFICATION_CLICK"/>
      <category android:name="android.intent.category.DEFAULT"/>
  </intent-filter>
</activity>

iOS 設定

請開啟 ios/Runner/AppDelegate.swift

// 首先上方先 import 所需函式
import flutter_local_notifications

// 
{
  // 加入這行
  FlutterLocalNotificationsPlugin.setPluginRegistrantCallback { 
    registry in GeneratedPluginRegistrant.register(with: registry)
  }
  GeneratedPluginRegistrant.register(with: self)
  // 也加入這行
  if #available(iOS 10.0, *) {
    UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate
  }
}

初始化

請在 lib 底下建立一個 plugins 資料夾,於其中建立一個檔案 notification.dart ,並請參考下方程式碼:

import 'package:flutter_local_notifications/flutter_local_notifications.dart';

class NotificationPlugin {
  final FlutterLocalNotificationsPlugin np = FlutterLocalNotificationsPlugin();
  
  init() async {
    // 在 Android 平台上的設置,並使用 flutter logo 作為通知顯示的圖標
    var android = const AndroidInitializationSettings('flutter_logo');

    // 在 iOS 平台上的設置,並設置了 iOS 通知的權限
    var ios = DarwinInitializationSettings(
      requestAlertPermission: true,
      requestBadgePermission: true,
      requestSoundPermission: true,
      onDidReceiveLocalNotification: (id, title, body, payload) async {},
    );

    // 註冊不同平台初始化設置的 InitializationSetting 對象
    var initSettings = InitializationSettings(android: android, iOS: ios);

    // 使用 np 實例的 initialize 方法初始化本地通知插件
    await np.initialize(
      initSettings,
      onDidReceiveNotificationResponse: (NotificationResponse notificationResponse) async {}
    );
  }
  // 觸發通知時,透過呼叫此函式來顯示通知
  Future showNotification(
      {int id = 0, String? title, String? body, String? payload}) async {
    return np.show(id, title, body, await notificatinoDetails());
  }

  notificatinoDetails() async {
    return const NotificationDetails(
      android: AndroidNotificationDetails('channelId', 'channelName',
          importance: Importance.max),
      iOS: DarwinNotificationDetails(),
    );
  }
}

接下來請開啟 main.dart 的主函式:


void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
  // 加入這行,使得 NotificationPlugin 呼叫 init 將本地通知註冊於應用程式中
  await NotificationPlugin().init();
  runApp(ChangeNotifierProvider(
      create: (context) => ThemeModel(), child: const MyApp()));
}

發出通知

接下來讓我們發出第一則通知吧。我們希望當開啟「每日一報」功能時,跳出一個通知表示該功能已開啟。請開啟 profile_screen.dart

// 請移至每日一報的 CupertinoSwitch
CupertinoSwitch(
  value: _isDailyReport,
  onChanged: (bool? value) {
    if (value == true) {
      NotificationPlugin().showNotification(
        title: '每日一報',
        body: '您訂閱的新聞已更新',
      );
    }
    setState(() {
      _isDailyReport = value!;
    });
})),

當 switch 狀態開啟時,則跳出通知。結果如下圖:
https://i.imgur.com/OHdZ2eF.gif

當你第一次觸發 iOS 平台的通知時,系統會詢問是否允許通知,請點選允許。Android 平台我自己在測試時有發現不一定會跳出詢問通知的對話框,因預設皆為不允許所以可能都不會跳出通知。請至手機的通知設定頁面將該應用的通知功能開啟。

註冊排程通知

假設我今天在提醒事項的應用程式中設定 20 分鐘後提醒我要記得發一篇鐵人賽文章,那麼在設定的當下就是將這個提醒的通知進行排程,並計算 20 分鐘後觸發。

flutter_local_notification 同樣也有此支援,讓我們來看看要怎麼做吧。

  1. 設定時區

請打開 main.dart ,並添加以下程式碼:

// 引入 timezone 相關的套件
import 'package:timezone/data/latest_all.dart' as tz;
import 'package:timezone/timezone.dart' as tz;

void main() async {
  tz.initializeTimeZones(); // 初始化時區資料庫
  tz.setLocalLocation(tz.getLocation('Asia/Taipei')); // 將時區設定為台北標準時間
  // 以下皆維持原狀故省略
}
  1. 建立排程通知

請開啟 notification.dart 檔案,並於 class 中加入以下程式碼:

Future showScheduledNotification(
      {int id = 0,
      String? title,
      String? body,
      String? payload,
      required DateTime scheduledDate}) async {
    return np.zonedSchedule(
      id,
      title,
      body,
      tz.TZDateTime.from(scheduledDate, tz.local),
      await notificatinoDetails(),
      androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
      uiLocalNotificationDateInterpretation: UILocalNotificationDateInterpretation.absoluteTime);
  }

與原先的 showNotification 大致無異。唯一有差別的為新增傳入一參數,用於表示要觸發該通知的時間戳記。

如此便可以使用該函式進行排程的通知,如下:

showScheduledNotification(
  title: '提醒我記得發鐵人賽文章',
  body: '第 23 天了,快要結束了~~',
  // 將排程時間設為現在之後的 20 分鐘
  scheduledDate: DateTime.now().add(const Duration(minutes: 20)),
);

這裡讓我們補充一下 np.zonedScheduleandroidScheduleModeuiLocalNotificationDateInterpretation 吧!

androidScheduleMode

此一參數用於確定發送通知的精確程度。為什麼需要提供此一參數呢?發通知不是很簡單的事情嗎?其實不然,從 Android 6.0 版本之後提供了兩種省電模式,稱作 DozeApp Standby

Doze 按字面照翻就是小歇一下,也就是當手機未處於充電狀態、閒置一段時間且螢幕關閉時,為了延續電池壽命,系統會限制應用程式取用網路資源或是過度消耗 CPU 運算的工作。當螢幕再次點亮或是系統自動同步的維護時間週期一到時,才會再次的進行同步處理,並允許應用程式取用網路。
https://ithelp.ithome.com.tw/upload/images/20231008/20135082OBZYYXbJPL.png
當維護完成後,系統會再次歇息,並暫停網路、同步處理等等的工作。系統會隨著時間逐漸拉長維護工作的週期。

App Standby 則是系統判斷將未使用的應用程式切換的待命模式。

該參數提供以下幾種屬性可以進行設定

屬性 通知時間 低功率環境下是否執行 是否需特殊權限
alarmClock 精確時間 需 SCHEDULE_EXAVT_ALARM 權限
exact 精確時間
exactAllowWhileIdle 精確時間
inexact 大致精確時間
inexactAllowWhileIdle 大致精確時間

uiLocalNotificationDateInterpretation

用於支援 iOS 版本低於 10 的設定,提供兩種屬性 absoluteTime 表示標準時間;wallClockTime 表示當前裝置時間。

了解完後,讓我們來測試一下吧!請開啟 profile_screen.dart 並移至每日一報觸發通知的函式,修改成以下程式碼:

// 請移至每日一報的 CupertinoSwitch
CupertinoSwitch(
  value: _isDailyReport,
  onChanged: (bool? value) {
    if (value == true) {
      NotificationPlugin().showScheduledNotification(
        title: '每日一報',
        body: '您可以試試把應用程式從後台移除,你應該還是會接收到通知',
      );
    }
    setState(() {
      _isDailyReport = value!;
    });
})),

不過目前的通知都是只有單次性的,一但觸發完畢就結束了。所以想要達成「每日一報」,我們需要使用到的是週期性通知。

註冊週期性通知

方法為 periodicallyShow() ,需要額外傳入一參數 RepeatInterval ,其屬性有:

  • everyMinute - 每分鐘通知
  • hourly - 每小時通知
  • daily - 每天通知
  • weekly - 每週通知

請參考下列程式碼:

Future showPeriodicNotification(
      {int id = 0,
      String? title,
      String? body,
      String? payload,
      required RepeatInterval interval}) async {
    return np.periodicallyShow(
      id,
      title,
      body,
      interval,
      await notificatinoDetails(),
      androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
    );

因此我們就可以串接此函式來達成每日提醒拉~

// 請移至每日一報的 CupertinoSwitch
CupertinoSwitch(
  value: _isDailyReport,
  onChanged: (bool? value) {
    if (value == true) {
      NotificationPlugin().showPeriodicNotification(
        title: '每日一報',
        body: '我們為您準備好了專屬於您的新聞,歡迎您來看看!',
        interval: RepeatInterval.daily
      );
    }
    setState(() {
      _isDailyReport = value!;
    });
})),

今日總結

今天我們算是完整的認識並使用了 flutter_local_notifications 的功能,可以方便我們用於實現本機的通知。

不過有些人可能會覺得尚有些單調,由於其只能顯示 title, body 等純文字內容。這裡也給各位一個替代的套件,名叫 awesome_notifications,就可以做出圖文並茂的通知拉!就交給有興趣的讀者自行研究。

今天的參考程式碼:https://github.com/ChungHanLin/micro_news_tutorial/tree/day23/micro_news_app


上一篇
[Day 22] 實戰新聞 APP - 使用 State Management 來觸發 Dark Mode
下一篇
[Day 24] 實戰新聞 APP - 擴充使用者資料 Part 1 (設定 Firestore)
系列文
Flutter 從零到實戰 - 30 天の學習筆記30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言